/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.client.diamondtouch;

import de.sciss.net.OSCListener;
import de.sciss.net.OSCMessage;
import de.sciss.net.OSCServer;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import locusts.client.diamondtouch.DTBoundingBoxEvent.Action;
import locusts.client.diamondtouch.DTSegmentsEvent.Segment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that handles communication with a DiamondTouch table via DTBridge or
 * some other OSC based relay. <tt>DTHandler</tt> starts an OSC server on the
 * required port and waits for the relay software tos end event packets over
 * UDP. These are decoded and instantiated as <tt>DTEvent</tt> objects.
 *
 * The class employs the observer/observable pattern
 * 
 * @author hamish
 */
public class DTHandler {

    //
    private static final int DEFAULT_PORT = 3333;
    private static final String DEFAULT_PROTOCOL = OSCServer.UDP;
    //
    private static final transient Logger LOG = LoggerFactory.getLogger(
            DTHandler.class.getName());
    //
    private final List<DTListener> listeners =
            new CopyOnWriteArrayList<DTListener>();
    //
    private final OSCServer oscServer;
    private final int port;
    private final String protocol;
    private DTTransform transform;

    public DTHandler(int port) throws IOException {
        this.port = port;
        protocol = DEFAULT_PROTOCOL;
        oscServer = OSCServer.newUsing(protocol, port, true);
        oscServer.addOSCListener(new OSCInput());
        transform = new DTTransform();
    }

    public DTHandler() throws IOException {
        this(DEFAULT_PORT);
    }

    public DTTransform getTransform() {
        return transform;
    }

    public void setTransform(DTTransform transform) {
        this.transform = transform;
    }

    public int getPort() {
        return port;
    }

    public String getProtocol() {
        return protocol;
    }

    public void start() {
        try {
            LOG.info("DiamondTouch Handler starting...");
            oscServer.start();
        } catch (IOException ex) {
            LOG.error(null, ex);
        }
    }

    public void stop() {
        try {
            LOG.info("DiamondTouch Handler stopping...");
            oscServer.stop();
        } catch (IOException ex) {
            LOG.error(null, ex);
        } finally {
            oscServer.dispose();
        }
    }

    private final class OSCInput implements OSCListener {

        public void messageReceived(OSCMessage m, SocketAddress addr,
                long time) {
            try {
                DTEvent e = createEvent(m, time);
                fireEvent(e);
            } catch (Exception e) {
                LOG.warn("Ignoring OSC Message due to an exception: ", e);
            }
        }
    }
    private static final String BOUNDING_BOX_NAME = "/DTBoundingBox";
    private static final String SEGMENTS_NAME = "/DTSegments";
    private static final int TABLE_SIZE_INDEX = 0;
    private static final int USERID_INDEX = 2;
    private static final int ACTION_INDEX = 3;
    private static final int ORIGIN_INDEX = 4;
    private static final int CORNER_INDEX = 6;
    private static final int NUM_X_SEGS_INDEX = 3;
    private static final int NUM_Y_SEGS_INDEX = 4;
    //

    private DTEvent createEvent(OSCMessage m, long time) throws
            IllegalArgumentException {
        final String name = m.getName();
        if (name.equals(SEGMENTS_NAME)) {
            return createSegmentsEvent(m, time);
        } else if (name.equals(BOUNDING_BOX_NAME)) {
            return createBoundingBoxEvent(m, time);
        } else {
            throw new IllegalArgumentException("Unknown message name: " + name);
        }
    }

    private Point2D readPoint(OSCMessage message, int index) throws
            ClassCastException {
        final Point2D point = new Point2D.Double(
                (Integer) message.getArg(index),
                (Integer) message.getArg(index + 1));
//        System.out.print("Point before: " + point);
        transform.transform(point);
//        System.out.println(", after: " + point);
        return point;
    }

    private void readTableSize(OSCMessage message, int index) throws
            ClassCastException {
        final int tableWidth = (Integer) message.getArg(index);
        final int tableHeight = (Integer) message.getArg(index + 1);
        transform.setTableSize(tableWidth, tableHeight);

    }

    private DTBoundingBoxEvent createBoundingBoxEvent(OSCMessage message,
            long time) throws IllegalArgumentException {
        if (message == null)
            throw new IllegalArgumentException(
                    "OSCMessage argument was null.");
        if (!message.getName().equals(BOUNDING_BOX_NAME))
            throw new IllegalArgumentException(
                    "OSC Message argument is not a DTBoundingBox message.");
        if (message.getArgCount() != 8)
            throw new IllegalArgumentException(
                    "OSC Message should contain 8 arguments, found:" +
                    message.getArgCount());
        readTableSize(message, TABLE_SIZE_INDEX);
        final int userId = (Integer) message.getArg(USERID_INDEX);
        final String actionStr = (String) message.getArg(ACTION_INDEX);
        final Action action = Action.valueOf(actionStr.toUpperCase());
        final Point2D origin = readPoint(message, ORIGIN_INDEX);
        final Point2D corner = readPoint(message, CORNER_INDEX);
        return new DTBoundingBoxEvent(this, userId,
                time, action,
                (int) origin.getX(), (int) origin.getY(),
                (int) corner.getX(), (int) corner.getY());
    }

    private DTSegmentsEvent createSegmentsEvent(OSCMessage message, long time)
            throws IllegalArgumentException {
        if (message == null)
            throw new IllegalArgumentException(
                    "OSCMessage argument was null.");
        if (!message.getName().equals(SEGMENTS_NAME))
            throw new IllegalArgumentException(
                    "OSC Message argument is not a DTBoundingBox message.");
        if (message.getArgCount() < 5)
            throw new IllegalArgumentException(
                    "OSCMessage parameter should contain at least 5 arguments; found:" +
                    message.getArgCount());
        readTableSize(message, TABLE_SIZE_INDEX);
        final int userId = (Integer) message.getArg(USERID_INDEX);
        final int numXSegs = (Integer) message.getArg(NUM_X_SEGS_INDEX);
        final int numYSegs = (Integer) message.getArg(NUM_Y_SEGS_INDEX);
        final int expectedArgCount = NUM_Y_SEGS_INDEX + numYSegs + numXSegs +
                1;
        if (message.getArgCount() < expectedArgCount)
            throw new IllegalArgumentException(
                    "OSCMessage parameter should contain " + expectedArgCount +
                    " arguments; found:" + message.getArgCount());

        final Segment[] xSegs = new Segment[numXSegs];
        final Segment[] ySegs = new Segment[numYSegs];


        final int xoffset = NUM_X_SEGS_INDEX + 1;
        for (int i = 0; i < xSegs.length; i++)
            xSegs[i] = readSegment(message, xoffset + i * 5, true);
        final int yoffset = xoffset + xSegs.length;
        for (int i = 0; i < ySegs.length; i++)
            ySegs[i] = readSegment(message, yoffset + i * 5, false);

        return new DTSegmentsEvent(this, userId, time, xSegs, ySegs);
    }

    private Segment readSegment(OSCMessage message, int index,
            boolean isXSegment) {
        final int first = (Integer) message.getArg(index);
        final int last = (Integer) message.getArg(index + 1);
        final int maxSignal = (Integer) message.getArg(index + 2);
        final int peak = (Integer) message.getArg(index + 3);
        final int secondPeak = (Integer) message.getArg(index + 4);
        if (isXSegment) {
            return new Segment(
                    transform.transformX(first), transform.transformX(last),
                    maxSignal, transform.transformX(peak),
                    secondPeak == -1 ? -1 : transform.transformX(secondPeak));
        } else {
            return new Segment(
                    transform.transformY(first), transform.transformY(last),
                    maxSignal, transform.transformY(peak),
                    secondPeak == -1 ? -1 : transform.transformY(secondPeak));

        }

    }

    public void addListener(DTListener listener) {
        if (listener == null)
            throw new NullPointerException("Listener argument is null.");
        if (!listeners.contains(listener)) listeners.add(listener);
    }

    public void removeListener(DTListener listener) {
        if (listener == null)
            throw new NullPointerException("Listener argument is null.");
        if (listeners.contains(listener)) listeners.remove(listener);
    }

    private void fireEvent(DTEvent e) {

        LOG.debug("Firing Event: {}", e);
        if (e.getClass().equals(DTBoundingBoxEvent.class)) {
            final DTBoundingBoxEvent be = (DTBoundingBoxEvent) e;
            switch (be.getAction()) {
                case DOWN:
                    for (DTListener listener : listeners)
                        listener.boundingBoxDown(be);
                    break;
                case UP:
                    for (DTListener listener : listeners)
                        listener.boundingBoxUp(be);
                    break;
                case MOVE:
                    for (DTListener listener : listeners)
                        listener.boundingBoxMoved(be);
                    break;
            }
        } else if (e.getClass().equals(DTSegmentsEvent.class)) {
            final DTSegmentsEvent se = (DTSegmentsEvent) e;
            for (DTListener listener : listeners)
                listener.segmentsReceived(se);
        }
    }
}
